package com.sheng.project.demo.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.util.Base64Utils;
import sun.security.pkcs.ContentInfo;
import sun.security.pkcs.PKCS7;
import sun.security.pkcs.PKCS9Attributes;
import sun.security.pkcs.SignerInfo;
import sun.security.util.DerValue;
import sun.security.util.ObjectIdentifier;
import sun.security.x509.AlgorithmId;
import sun.security.x509.X500Name;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Enumeration;
import java.util.Locale;

/**
 * RSA非对称算法
 * 生成公私钥
 * 加解密
 */
@Slf4j
public class RSAUtilV2 {

    /**
     * 默认的签名算法“SHA1WithRSA”
     * “SHA1WithRSA”算法，采用同一个私钥签名同一段文字，每次所得的签名都一样。
     */
    public static final String ALGORITHM_SHA1_WITH_RSA = "SHA1withRSA";
    public static final String ALGORITHM_SHA256_WITH_RSA = "SHA256withRSA";
    /**
     * 签名算法“SHA256withRSAandMGF1”
     * 此算法具有更高的安全级别，采用“SHA256withRSAandMGF1”算法，则会使得每次采用同一个私钥
     * 签名同一段文字，所得的签名值都是不同的。
     */
    public static final String ALGORITHM_SHA256_WITH_RSA_AND_MGF1 = "SHA256withRSAandMGF1";
    private static final String ALGORITHM_RSA = "RSA";
    private static final int ALGORITHM_RSA_KEY_SIZE = 512;
    private static final String TRANSFORMATION_RSA = "RSA/ECB/PKCS1Padding";
    private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();

    /**
     * 从文件中读取秘钥
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public KeyPair getKeyPair(String keyStorePath, String keyStorePassword)
            throws Exception {
        KeyStore keyStore = this.getKeyStore(keyStorePath, keyStorePassword);
        return this.getKeyPair(keyStore, keyStorePassword);
    }

    /**
     * 从文件中读取秘钥
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public X509Certificate[] getCertificates(String keyStorePath, String keyStorePassword)
            throws Exception {
        KeyStore keyStore = this.getKeyStore(keyStorePath, keyStorePassword);
        return this.getCertificates(keyStore);
    }

    /**
     * 从文件中读取秘钥
     *
     * @param keyStorePath
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public KeyStore getKeyStore(String keyStorePath, String keyStorePassword)
            throws Exception {
        log.info("加载证书==>{},{}", keyStorePath, keyStorePassword);
        KeyStore keyStore = null;
        if (keyStorePath.toLowerCase(Locale.ENGLISH).endsWith(".pfx")) {
            keyStore = keyStore.getInstance("PKCS12");
        } else {
            keyStore = keyStore.getInstance("JKS");
        }
        FileInputStream fis = FileUtils.openInputStream(FileUtils.getFile(keyStorePath));
        keyStore.load(fis, keyStorePassword.toCharArray());
        IOUtils.closeQuietly(fis);
        return keyStore;
    }

    /**
     * @param keyStore
     * @param keyStorePassword
     * @return
     * @throws Exception
     */
    public KeyPair getKeyPair(KeyStore keyStore, String keyStorePassword) {
        try {
            Enumeration<String> aliases = keyStore.aliases();
            String keyAlias = null;
            if (null != aliases) {
                while (aliases.hasMoreElements()) {
                    keyAlias = aliases.nextElement();
                    Certificate[] certs = keyStore.getCertificateChain(keyAlias);
                    if (null == certs || certs.length == 0) {
                        continue;
                    }
                    X509Certificate cert = (X509Certificate) certs[0];
                    if (this.matchUsage(cert.getKeyUsage(), 1)) {
                        try {
                            cert.checkValidity();
                            break;
                        } catch (CertificateException ex) {
                            continue;
                        }
                    }
                }
            }
            X509Certificate[] certificates = null;
            if (keyStore.isKeyEntry(keyAlias)) {
                Certificate[] certs = keyStore.getCertificateChain(keyAlias);
                for (int i = 0; i < certs.length; i++) {
                    if (!(certs[i] instanceof X509Certificate)) {
                        throw new IllegalAccessError("证书链接所指的证书不符合x509格式！");
                    }
                }
                certificates = new X509Certificate[certs.length];
                for (int i = 0; i < certs.length; i++) {
                    certificates[i] = (X509Certificate) certs[i];
                }
            } else if (keyStore.isCertificateEntry(keyAlias)) {
                Certificate cert = keyStore.getCertificate(keyAlias);
                if (cert instanceof X509Certificate) {
                    certificates = new X509Certificate[]{(X509Certificate) cert};
                }
            } else {
                throw new IllegalAccessError("keystore存储的证书不合法！");
            }
            PrivateKey privateKey = (PrivateKey) keyStore.getKey(keyAlias, keyStorePassword.toCharArray());
            PublicKey publicKey = certificates[0].getPublicKey();
            return new KeyPair(publicKey, privateKey);
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("解析签名证书的密钥失败");
    }

    /**
     * @param keyUsage
     * @param usage
     * @return
     */
    private boolean matchUsage(boolean[] keyUsage, int usage) {
        if (0 == usage || null == keyUsage) {
            return true;
        }
        for (int i = 0; i < Math.min(keyUsage.length, 32); i++) {
            if ((usage & (1 << i)) != 0 && !keyUsage[i]) {
                return false;
            }
        }
        return true;
    }

    /**
     * @param keyStore
     * @return
     */
    public X509Certificate[] getCertificates(KeyStore keyStore) {
        try {
            Enumeration<String> aliases = keyStore.aliases();
            String keyAlias = null;
            if (null != aliases) {
                while (aliases.hasMoreElements()) {
                    keyAlias = aliases.nextElement();
                    Certificate[] certs = keyStore.getCertificateChain(keyAlias);
                    if (null == certs || certs.length == 0) {
                        continue;
                    }
                    X509Certificate cert = (X509Certificate) certs[0];
                    if (this.matchUsage(cert.getKeyUsage(), 1)) {
                        try {
                            cert.checkValidity();
                            break;
                        } catch (CertificateException ex) {
                            continue;
                        }
                    }
                }
            }
            X509Certificate[] certificates = null;
            if (keyStore.isKeyEntry(keyAlias)) {
                Certificate[] certs = keyStore.getCertificateChain(keyAlias);
                for (int i = 0; i < certs.length; i++) {
                    if (!(certs[i] instanceof X509Certificate)) {
                        throw new IllegalAccessError("证书链接所指的证书不符合x509格式！");
                    }
                }
                certificates = new X509Certificate[certs.length];
                for (int i = 0; i < certs.length; i++) {
                    certificates[i] = (X509Certificate) certs[i];
                }
            } else if (keyStore.isCertificateEntry(keyAlias)) {
                Certificate cert = keyStore.getCertificate(keyAlias);
                if (cert instanceof X509Certificate) {
                    certificates = new X509Certificate[]{(X509Certificate) cert};
                }
            } else {
                throw new IllegalAccessError("keystore存储的证书不合法！");
            }
            return certificates;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("解析签名证书的密钥失败");
    }

    /**
     * @param keyStore
     * @return
     */
    public String getCertId(KeyStore keyStore) {
        try {
            Enumeration<String> aliasenum = keyStore.aliases();
            String keyAlias = null;
            if (aliasenum.hasMoreElements()) {
                keyAlias = aliasenum.nextElement();
            }
            X509Certificate cert = (X509Certificate) keyStore.getCertificate(keyAlias);
            return cert.getSerialNumber().toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("解析签名证书的certId失败");
    }

    /**
     * 通过证书路径初始化为公钥证书
     *
     * @param path
     * @return
     */
    public X509Certificate getCertificateFromPath(String path) {
        log.info("加载X509Certificate证书文件==>{}", path);
        X509Certificate x509Cert = null;
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509", PROVIDER);
            FileInputStream fis = FileUtils.openInputStream(FileUtils.getFile(path));
            x509Cert = (X509Certificate) cf.generateCertificate(fis);
            IOUtils.closeQuietly(fis);
            // 打印证书加载信息,供测试阶段调试
            log.info("[{}][CertId={}]", path, x509Cert.getSerialNumber().toString());
            return x509Cert;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("加载X509Certificate证书失败。");
    }

    /**
     * 将字符串转换为X509Certificate对象.
     *
     * @param x509CertString
     * @return
     */
    public X509Certificate getCertificateFromString(String x509CertString) {
        X509Certificate x509Cert = null;
        try {
            CertificateFactory cf = CertificateFactory.getInstance("X.509", PROVIDER);
            InputStream is = IOUtils.toInputStream(x509CertString, StandardCharsets.ISO_8859_1.name());
            x509Cert = (X509Certificate) cf.generateCertificate(is);
            IOUtils.closeQuietly(is);
            // 打印证书加载信息,供测试阶段调试
            log.info("[CertId={}]", x509Cert.getSerialNumber().toString());
            return x509Cert;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("加载X509Certificate证书字符串失败。");
    }

    /**
     * 获取公钥
     *
     * @param publicKeyBase64 公钥Base64密文
     * @return 公钥
     */
    public RSAPublicKey getPublicKey(String publicKeyBase64) {
        try {
            byte[] keyBytes = Base64Utils.decodeFromString(publicKeyBase64.replaceAll("\r|\n", ""));
            PublicKey publicKey = KeyFactory.getInstance(ALGORITHM_RSA, PROVIDER)
                    .generatePublic(new X509EncodedKeySpec(keyBytes));
            return (RSAPublicKey) publicKey;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("Base64转RSA公钥失败。");
    }

    /**
     * 获取私钥
     *
     * @param privateKeyBase64 私钥Base64密文
     * @return 私钥
     */
    public RSAPrivateKey getPrivateKey(String privateKeyBase64) {
        try {
            byte[] keyBytes = Base64Utils.decodeFromString(privateKeyBase64.replaceAll("\r|\n", ""));
            PrivateKey privateKey = KeyFactory.getInstance(ALGORITHM_RSA, PROVIDER)
                    .generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
            return (RSAPrivateKey) privateKey;
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("Base64转RSA私钥失败。");
    }

    /**
     * 公钥加密过程
     *
     * @param plainText       明文
     * @param publicKeyBase64 公钥
     * @return 密文
     * @throws Exception 加密过程中的异常信息
     */
    public String encryptByPublicKey(String plainText, String publicKeyBase64) {
        return this.encryptByPublicKey(plainText, this.getPublicKey(publicKeyBase64));
    }

    /**
     * 公钥加密过程
     *
     * @param plainText 明文
     * @param publicKey 公钥
     * @return 密文
     * @throws Exception 加密过程中的异常信息
     */
    public String encryptByPublicKey(String plainText, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA);
            cipher.init(Cipher.ENCRYPT_MODE, publicKey);
            byte[] plainTextData = plainText.replaceAll("\r|\n", "").getBytes(StandardCharsets.UTF_8);
            int singleMaxLength = publicKey.getModulus().bitLength() / 8 - 11;
            if (plainTextData.length < singleMaxLength) {
                byte[] cipherTextData = cipher.doFinal(plainTextData);
                return Base64Utils.encodeToString(cipherTextData);
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int cycleLength = (plainTextData.length + 1) / singleMaxLength;
                // 分段加密
                for (int i = 0; i < cycleLength + 1; i++) {
                    byte[] cipherTextDataItem = cipher.doFinal(plainTextData,
                            singleMaxLength * i,
                            Math.min(singleMaxLength, plainTextData.length - singleMaxLength * i));
                    baos.write(cipherTextDataItem);
                }
                baos.close();
                byte[] cipherTextData = baos.toByteArray();
                return Base64Utils.encodeToString(cipherTextData);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("RSA公钥加密失败。");
    }

    /**
     * 私钥加密过程
     *
     * @param plainText        明文
     * @param privateKeyBase64 私钥
     * @return 密文
     * @throws Exception 加密过程中的异常信息
     */
    public String encryptByPrivateKey(String plainText, String privateKeyBase64) {
        return this.encryptByPrivateKey(plainText, this.getPrivateKey(privateKeyBase64));
    }

    /**
     * 私钥加密过程
     *
     * @param plainText  明文
     * @param privateKey 私钥
     * @return 密文
     * @throws Exception 加密过程中的异常信息
     */
    public String encryptByPrivateKey(String plainText, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA);
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            byte[] plainTextData = plainText.replaceAll("\r|\n", "").getBytes(StandardCharsets.UTF_8);
            int singleMaxLength = privateKey.getModulus().bitLength() / 8 - 11;
            if (plainTextData.length < singleMaxLength) {
                byte[] cipherTextData = cipher.doFinal(plainTextData);
                return Base64Utils.encodeToString(cipherTextData);
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int cycleLength = (plainTextData.length + 1) / singleMaxLength;
                // 分段加密
                for (int i = 0; i < cycleLength + 1; i++) {
                    byte[] cipherTextDataItem = cipher.doFinal(plainTextData,
                            singleMaxLength * i,
                            Math.min(singleMaxLength, plainTextData.length - singleMaxLength * i));
                    baos.write(cipherTextDataItem);
                }
                baos.close();
                byte[] cipherTextData = baos.toByteArray();
                return Base64Utils.encodeToString(cipherTextData);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("RSA私钥加密失败。");
    }

    /**
     * 私钥解密过程
     *
     * @param cipherText       密文
     * @param privateKeyBase64 私钥
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public String decryptByPrivateKey(String cipherText, String privateKeyBase64) {
        return this.decryptByPrivateKey(cipherText, this.getPrivateKey(privateKeyBase64));
    }

    /**
     * 私钥解密过程
     *
     * @param cipherText 密文
     * @param privateKey 私钥
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public String decryptByPrivateKey(String cipherText, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA);
            cipher.init(Cipher.DECRYPT_MODE, privateKey);
            //该密钥能够解密的最大字节长度
            int singleMaxLength = privateKey.getModulus().bitLength() / 8;
            byte[] cipherTextData = Base64Utils.decodeFromString(cipherText.replaceAll("\r|\n", ""));
            if (cipherTextData.length < singleMaxLength) {
                byte[] plainTextData = cipher.doFinal(cipherTextData);
                return new String(plainTextData, StandardCharsets.UTF_8);
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int cycleLength = (cipherTextData.length + 1) / singleMaxLength;
                // 分段解密
                for (int i = 0; i < cycleLength; i++) {
                    byte[] plainTextDataItem = cipher.doFinal(cipherTextData,
                            singleMaxLength * i,
                            Math.min(singleMaxLength, cipherTextData.length - singleMaxLength * i));
                    baos.write(plainTextDataItem);
                }
                baos.close();
                byte[] plainTextData = baos.toByteArray();
                return new String(plainTextData, StandardCharsets.UTF_8);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("RSA私钥解密失败。");
    }

    /**
     * 公钥解密过程
     *
     * @param cipherText      密文
     * @param publicKeyBase64 公钥
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public String decryptByPublicKey(String cipherText, String publicKeyBase64) {
        return this.decryptByPublicKey(cipherText, this.getPublicKey(publicKeyBase64));
    }

    /**
     * 公钥解密过程
     *
     * @param cipherText 密文
     * @param publicKey  公钥
     * @return 明文
     * @throws Exception 解密过程中的异常信息
     */
    public String decryptByPublicKey(String cipherText, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance(TRANSFORMATION_RSA);
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            //该密钥能够解密的最大字节长度
            int singleMaxLength = publicKey.getModulus().bitLength() / 8;
            byte[] cipherTextData = Base64Utils.decodeFromString(cipherText.replaceAll("\r|\n", ""));
            if (cipherTextData.length < singleMaxLength) {
                byte[] plainTextData = cipher.doFinal(cipherTextData);
                return new String(plainTextData, StandardCharsets.UTF_8);
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                int cycleLength = (cipherTextData.length + 1) / singleMaxLength;
                // 分段解密
                for (int i = 0; i < cycleLength; i++) {
                    byte[] plainTextDataItem = cipher.doFinal(cipherTextData,
                            singleMaxLength * i,
                            Math.min(singleMaxLength, cipherTextData.length - singleMaxLength * i));
                    baos.write(plainTextDataItem);
                }
                baos.close();
                byte[] plainTextData = baos.toByteArray();
                return new String(plainTextData, StandardCharsets.UTF_8);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        throw new IllegalArgumentException("RSA公钥解密失败。");
    }

    /**
     * 采用特定的签名算法，通过私钥进行RSA签名
     *
     * @param content       待签名数据
     * @param privateKey    私钥
     * @param signAlgorithm 签名算法
     * @return 经过BASE64编码之后的签名值
     */
    public String signByPrivateKey(byte[] content, String signAlgorithm,
                                   RSAPrivateKey privateKey) {
        try {
            Signature signature = Signature.getInstance(signAlgorithm, PROVIDER);
            signature.initSign(privateKey);
            signature.update(content);
            return Base64Utils.encodeToString(signature.sign());
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("RSA签名失败：" + e.getLocalizedMessage());
        }
    }

    /**
     * 采用特定的签名算法，通过私钥进行RSA签名
     *
     * @param content       待签名数据
     * @param rsaPrivateKey 私钥
     * @param signAlgorithm 签名算法
     * @param certificates  证书
     * @return 经过BASE64编码之后的签名值
     */
    public String signByPrivateKey(byte[] content, String signAlgorithm,
                                   RSAPrivateKey rsaPrivateKey, X509Certificate[] certificates) {
        try {
            // 不能指定PROVIDER
            Signature signature = Signature.getInstance(signAlgorithm);
            signature.initSign(rsaPrivateKey);
            signature.update(content, 0, content.length);

            ContentInfo contentInfo = null;
            Field data_oidField = ContentInfo.class.getField("DATA_OID");
            Object data_oid = data_oidField.get(null);
            Constructor<ContentInfo> contentInfoConstructor = ContentInfo.class.getConstructor(data_oid.getClass(), DerValue.class);
            contentInfo = contentInfoConstructor.newInstance(data_oid, null);
            X509Certificate x509 = certificates[certificates.length - 1];
            BigInteger serial = x509.getSerialNumber();
            Constructor x500NameConstructor = X500Name.class.getConstructor(String.class);
            Object x500NameObject = x500NameConstructor.newInstance(x509.getIssuerDN().getName());
            Method algorithmIdGet = AlgorithmId.class.getMethod("get", String.class);
            Object digestAlgorithmId = algorithmIdGet.invoke(null, SHAUtilV2.ALGORITHM_SHA1);
            Field algorithmIdField = AlgorithmId.class.getField("RSAEncryption_oid");
            Object rsaOid = algorithmIdField.get(null);
            Constructor algorithmConstructor = AlgorithmId.class.getConstructor(ObjectIdentifier.class);
            Object algorithmRsaOid = algorithmConstructor.newInstance(rsaOid);
            Constructor<SignerInfo> signerInfoConstructor = SignerInfo.class.getConstructor(X500Name.class,
                    BigInteger.class, AlgorithmId.class, PKCS9Attributes.class, AlgorithmId.class,
                    byte[].class, PKCS9Attributes.class);
            SignerInfo si = signerInfoConstructor.newInstance(x500NameObject, serial,
                    digestAlgorithmId, null, algorithmRsaOid, signature.sign(), null);
            SignerInfo[] signerInfos = {si};
            Object digestAlgorithmIds = Array.newInstance(AlgorithmId.class, 1);
            Array.set(digestAlgorithmIds, 0, digestAlgorithmId);
            Constructor<PKCS7> pkcs7Constructor = PKCS7.class.getConstructor(digestAlgorithmIds.getClass(),
                    ContentInfo.class, X509Certificate[].class, signerInfos.getClass());
            PKCS7 p7 = pkcs7Constructor.newInstance(digestAlgorithmIds, contentInfo, certificates, signerInfos);
            ByteArrayOutputStream baout = new ByteArrayOutputStream();
            p7.encodeSignedData(baout);
            return Base64Utils.encodeToString(baout.toByteArray());
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("RSA签名失败：" + e.getLocalizedMessage());
        }
    }


    /**
     * 采用的特定的签名算法，通过公钥进行RSA验证签名
     *
     * @param content       待验签数据
     * @param sign          经过BASE64编码之后的签名值
     * @param signAlgorithm 签名算法
     * @param publicKey     公钥
     * @return 布尔值，true表示签名一致，false表示签名不一致
     */
    public boolean verifyByPublicKey(byte[] content, String sign, String signAlgorithm,
                                     RSAPublicKey publicKey) {
        try {
            Signature signature = Signature.getInstance(signAlgorithm, PROVIDER);
            signature.initVerify(publicKey);
            signature.update(content);
            return signature.verify(Base64Utils.decodeFromString(sign));
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalArgumentException("RSA验签失败：" + e.getLocalizedMessage());
        }
    }

}
